/*    Copyright 2010 Tobias Marschall
 *
 *    This file is part of MoSDi.
 *
 *    MoSDi is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    MoSDi is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with MoSDi.  If not, see <http://www.gnu.org/licenses/>.
 */

package mosdi.util;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.Stack;
import java.util.EmptyStackException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class Log {
	public static enum Level { STANDARD, VERBOSE, DEBUG };
	
	private static Stack<Long> times = null;
	private static Stack<Long> cpuTimes;
	private static boolean timingActive = false;
	private static Level currentLevel = Level.STANDARD;
	private static float lastPeriod = 0.0f;
	private static float lastPeriodCpu = 0.0f;
	
	// To prevent instantiation
	private Log() {}
	
	public static void startTimer() {
		if (!timingActive) return;
		times.push(System.currentTimeMillis());
        ThreadMXBean tb = ManagementFactory.getThreadMXBean();
        cpuTimes.push(tb.getCurrentThreadCpuTime());
	}
	
	public static void stopTimer(String message) {
		if (!timingActive) return;
		String s="";
		try {
			Long t0 = times.pop();
			long t = System.currentTimeMillis() - t0;
			Long t0cput = cpuTimes.pop();
	        ThreadMXBean tb = ManagementFactory.getThreadMXBean();
			long tCpu = tb.getCurrentThreadCpuTime() - t0cput;
			lastPeriod=t/1000.0f;
			lastPeriodCpu=((float)tCpu)*1e-9f;
			for (int i=0; i<times.size(); i++) {
				s+="   ";
			}
			s+=String.format(">T> %.3f %.3f (%s)", lastPeriod, lastPeriodCpu, message);
		} catch (EmptyStackException e) {
			s+=String.format(">T> NaN NaN (%s)", message);
		}
		System.out.println(s);
	}

	public static void restartTimer(String message) {
		if (!timingActive) return;
		stopTimer(message);
		startTimer();
	}

	public static Level getLogLevel() {
		return currentLevel;
	}

	public static boolean levelAtLeast(Level level) {
		return level.compareTo(currentLevel)<=0; 
	}
	
	public static void setLogLevel(Level currentLevel) {
		Log.currentLevel = currentLevel;
	}
	
	public static boolean isTimingActive() {
		return timingActive;
	}

	public static void setTimingActive(boolean timingActive) {
		Log.timingActive = timingActive;
		if (timingActive) {
			times = new Stack<Long>();
			cpuTimes = new Stack<Long>();
		}
	}
	
	public static void println(Level level, String s) {
		if (level.compareTo(currentLevel)<=0) {
			System.out.println(s);
		}
	}
	
	public static void print(Level level, String s) {
      if (level.compareTo(currentLevel)<=0) {
         System.out.print(s);
      }
	}
	
	 // %[argument_index$][flags][width][.precision]conversion
   private static final String formatSpecifier
       = "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([a-zA-Z%])";
   
   // %[argument_index$][width][.precision][tT]
   private static final String timeSpecifier = "%(\\d+\\$)?(\\d+)?(\\.\\d+)?[tT]";

   private static Pattern fsPattern = Pattern.compile(formatSpecifier);
   private static Pattern tPattern = Pattern.compile(timeSpecifier);
	
   /**
    * If the given level corresponds with the actual level, the method prints 
    * a formatted string using the specified format string and arguments.<br>
    * NOTE: The conversion %t is not used as usual in java. If times are requested
    * the result is formated as a decimal number in computerized scientific notation
    * and as 'NaN' else.
    * @param level
    *           Log level for that the output is requested.
    * @param s
    *           Format string.
    * @param args
    *           Arguments referenced by the format specifiers in the format string.
    * @see java.util.Formatter
    * @see #format(String, Object...)
    * @author Markus Kemmerling
    */
	public static void printf(Level level, String s, Object... args) {
	   if (level.compareTo(currentLevel)>0){ return;}
	   System.out.print(Log.format(s, args));
	}

	/**
    * Formats a string using the specified format string and arguments.<br>
    * NOTE: The conversion %t is not used as usual in java. If times are requested
    * the result is formated as a decimal number in computerized scientific notation
    * and as 'NaN' else.
    * @param s
    *           Format string.
    * @param args
    *           Arguments referenced by the format specifiers in the format string.
    * @see java.util.Formatter
    * @author Markus Kemmerling
    */
	public static String format(String s, Object... args) {
	   Matcher fsMatcher = fsPattern.matcher(s);
      Matcher tMatcher = tPattern.matcher(s);
      int stringIdx = 0;
      int argsIdx = -1;
      StringBuilder sb = new StringBuilder();
      
      while (stringIdx < s.length()) {
         if (fsMatcher.find(stringIdx) && tMatcher.find(stringIdx)) {
            argsIdx++;
            // at least one format specifier and one time specifier remain
            
            String[] sa = new String[5];
            for (int j = 0; j < fsMatcher.groupCount(); j++) {
               sa[j] = fsMatcher.group(j + 1);
            }
            
            int index = argsIdx;
            if (sa[0] != null) {
               try {
                  index = Integer.parseInt(sa[0].substring(0, sa[0].length() - 1)) - 1;
                  argsIdx--; // to be conform with java.util.Formatter; argsIdx was not used for this conversion 
               } catch (NumberFormatException x) {
                  assert (false);
               }
            }
            
            if (fsMatcher.start() == tMatcher.start() && fsMatcher.end() == tMatcher.end()) {
               // the founded format specifier is the founded time specifier
               if (timingActive) {
                  // replace the 't' conversion with an 'e' conversion
                  sa[4] = "e";
                  
               } else {
                  // replace the 't' conversion with a 's' conversion
                  sa[4] = "s";
                  // replace the corresponding argument with "NaN"
                  args[index] = "NaN";
               }
               sb.append(s.substring(stringIdx, fsMatcher.start())); // append everything before the match
               sb.append("%"); // % is not included in the groupes
               for (int i = 0; i < sa.length; i++) {
                  if (sa[i]!=null)
                     sb.append(sa[i]);
               }
            } else {
               // nothing to to do
               sb.append(s.substring(stringIdx, fsMatcher.end()));
            }
            // go to the next format specifier
            stringIdx = fsMatcher.end();
            

         } else {
            // no more format specifier ore time specifier remain
            sb.append(s.substring(stringIdx));
            break;
         }
      }
      
      return String.format(sb.toString(), args);
	}
	
	public static void errorln(String s) {
	   System.err.println(s);
	}
	
	public static void printException(Exception e) {
	   System.err.println(e.toString());
      for (StackTraceElement ste: e.getStackTrace() ){
          System.err.println("\tat " + ste);
       }
	}
	
	public static float getLastPeriod() {
		return lastPeriod;
	}

	public static float getLastPeriodCpu() {
		return lastPeriodCpu;
	}
}
